ES6 Module

ES6-Module

模块化规范

0.IIFE

1
2
3
4
5
6
7
const myModule = (function(...deps){
return {
hello : () => {
console.log("hello from myModule");
}
}
})(dependencies);

该方法只是把变量和方法都封装在了本身作用域内的一种模式,并没有构成处理依赖能力的模块。在一定程度上解决了作用域污染的问题。

1.CommonJS规范

Node.js平台的默认格式,可以在Node平台上运行,为了统一JavaScript在浏览器之外的实现,CommonJS诞生了。通常的写法是这样的:

1
2
3
4
5
6
7
8
9
10
//fileA.js
module.exports = {
hello:() => {
console.log("hello from myModule")
}
}

//fileB.js
const myModule = require("./fileA.js");
myModule.hello();

CommonJS规范是为了解决JS的作用域问题而定义的模块模式,可以使每个模块在自身的命名空间中执行。模块必须通过module.exports导出对外的变量或接口,通过require()来导入其他模块的输出到当前模块作用域中。

CommonJS是动态加载即运行时加载方案,在并不需要完全加载所有方法的前提下,仍然会加载所有的模块。如下例所示:

1
2
3
4
5
6
7
8
//CommonJS模块
let { state,exists,readFile } = require("fs");

//<==>等价于
let _fs = require("fs");
let state = _fs.state;
let exists = _fs.exists;
let readFile= _fs.readDile;

2.AMD规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//AMDTest.html
//...
<body>
<h1>我是测试Require.js的标题</h1>
<a href="https://corbusier.github.io/">My Blog</a>
<script src="AMD-main.js"></script>
</body>

//myName.js
define('myName',[],function(){
return "my name is KaKa";
});

//yourName.js
define('yourName',["jquery"],function(){
console.log($("a").attr("href"));
return 'Your name is KangKang';
});

//AMD-main.js
requirejs.config({
baseUrl: "./node_modules/jquery/",
paths:{
'myName': "../../myName",
'jquery': "dist/jquery",
'yourName': "../../yourName"
}
});

require(['myName','yourName','jquery'],function(myName,yourName,$){
console.log(myName);
console.log(yourName);
console.log($("h1").html());
});

而AMD方案也是一种动态异步加载方案,它们的API语义只有在运行时才会被考虑进来,因此可以在运行时修改一个模块的API。不管是CMD还是AMD规范,都是讲模块定义封装在一个API中,简要的说明该方案的核心概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var MyModule = (function Manager(){
var modules = {};
function define(name,deps,impl){
for(var i = 0;i<deps.length;i++){
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl,deps);
}

function get(name){
return modules[name];
}

return {
define,
get
};
})();

为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API。储存在根据名字来管理的模块列表中。在yourName.js中可以以jQuery实例作为依赖参数传入,并且相应的使用它。

3. CMD规范

并没有使用过UMD规范的模块化方案,但是定义方法与AMD相差不大,对于依赖的模块,AMD依赖前置,CMD依赖就近。

4. ES6 Module

ES6模块的设计思想是尽量静态化,在编译时就确定好模块之间的依赖关系,避免在运行时才抛出错误。除此之外,由于是静态加载,在CommonJS的例子中,如果改写为ES6模块:

1
import { state,exists,readFile } from "fs";

以上的实质是从fs模块中加载3个方法,其他方法并不会加载。所以加载的效率也更高。也不需要看上去冗余的define关键字来定义模块模式了。

ES6 Module命令

export

一个模块是一个独立的文件,该模块内部的所有变量,外部无法获取。如果希望其他的模块能够读取该模块内部的某个变量,就必须使用export关键字输出变量。

1
2
3
4
5
6
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

除了输出变量,还可以输出函数或类(class)。

1
2
3
export function add(a,b){
return a + b;
}

除了使用以上的方式暴露变量/函数外,还可以使用as关键字。

1
2
3
4
5
6
7
function v1() { ... }
function v2() { ... }

export {
v1 as streamV1,
v2 as streamV2
};

export命令可以处于模块的任何位置。但出现在块级作用域内就会报错,因为处于条件代码块中,无法做静态优化,违背了ES6 Module的设计初衷。

import命令

使用了export定义了对外接口以后,其他JS文件可以使用import命令加载模块。

1
2
3
4
5
6
// main.js
import {firstName, lastName, year} from './profile';

function setName(element) {
element.textContent = firstName + ' ' + lastName;
}

注意 : import关键字后{}中的变量名必须与export输出的变量名相同。

由于import是静态执行的,所以不能使用表达式和变量,他们是只能在运行时得到结果的语法结构。

最后,当使用ES6 Module时要注意script中的type属性:

1
2
//注意:type="module"
<script src="B.js" type="module"></script>

如果直接运行文件,会因为file://文件域的跨域问题出现报错,此时需要开一个server服务器,建议使用http-server:

1
2
3
npm install http-server 
//cmd中运行
http-server

export default命令

当使用import命令时,需要知道加载的变量名或函数名。通过default命令,可以在不需要知道函数或变量名名称的前提下,默认输出变量。

1
2
3
4
5
6
7
8
//export-defult.js
export defualt function(){
console.log("foo");
}

//import-defult.js
import customeName from './export-default';
customName();//'foo'

以上的代码中模块文件export-default.js默认输出一个函数,其他模块加载时,import命令可以为该匿名函数指定任意名字。

比如,加载一些常用的模块:

1
2
3
import $ from 'jquery';//加载jQuery库
import _ from 'lodash';//加载lodash
import moment from 'moment';//加载moment

注意,此时引入模块时,不需要使用大括号。

除此之外,export default也可以用在命名函数前。

1
2
3
export defualt function foo(){
console.log('foo');
}

export default命令用于指定模块的默认输出,一个模块只能有一个默认输出。因此export default命令只能使用一次,所以import命令后才不用加大括号,因为只会对应一个方法。

通配符

除了使用指定变量名或export default定义的导出,还可以使用*通配符加载模块的全部。

1
2
3
4
5
6
7
//math.js
export const add = (a,b) => a + b;
export const subtract = (a,b) => a - b;

import * as math from './math.js';
math.add(1,2);
math.subtract(1,2);

as关键字

允许在模块输入或者输出时使用as关键字修改名字。

1
2
3
4
5
6
7
8
//输入:  
import * as circle from './circle'

//输出:
function add(x,y){
return x + y;
}
export {add as plus};

特性

import导入模块只读

  1. 在CommonJS中,导入的模块是导出值的复制值,并且require动作是同步的。所以导入与导出之间的联系是不存在连接的。

  2. 在ES6中,到日对导出值是只读的。因此他们的关系是赋址。并且”只读”说明在导入的模块中不能直接修改被导入的值,如果要修改被导入的值,可以通过调用被导入模块的函数来达到目的。

例如:

1
2
3
4
5
6
7
//lib.js
export let obj = {};

//main.js
import {obj} from './lib';
obj.prop = 123;//正确
obj = {};//TypeError

支持循环依赖

模块A、B之间互相导入,并在其中调用两者之间的函数,形成循环调用。
ComonJS为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//a.js
var b = require('b');
function foo(){
b.bar();
}
exports.foo = foo;

//b.js
var a = require('a');
function bar(){
if(Math.random()){
a.foo();
}
}
exports.bar = bar;

在b模块成功导入a之前,b模块不能使用a模块的方法。而在b模块导入a模块时,a需要先加载完成自身的模块依赖,这时a执行var b = require(b);

而ES6自动支持循环依赖。import对导入的模块是只读的。所以在执行过程中可以间接调用导入的值。

//a.js
import {bar} from './b';
export function foo(){
    bar();
}

//b.js
import {foo} from 'a';
export function bar(){
    if(Math.random()){
        foo();
    }
}

结论

与其他规范的模块化规范相比,ES6 Module更强调静态化加载。这样做带来的优点有:

  • 1. 静态加载效率更高(能够实现按需加载)
  • 2. 未来引入宏、类型检查等特性(静态化)
  • 3. 前后端统一模块化标准
  • 4. 未来浏览器API及扩展功能通过模块提供

目前,通过http-server,ES6 Module可以在原生浏览器中直接运行。结合babel及webpack,ES6 Module能够提供更兼容、友好的模块化方案,而babelwebpack在这其中扮演了什么样的角色,在下一篇文章中会做出阐述。